<?php
/**
 * "Abstract" class for a number of different payment
 * types allowing a user to pay for something on a site.
 *
 * This can't be an abstract class because sapphire doesn't
 * support abstract DataObject classes.
 *
 * @package payment
 */
class Payment extends DataObject implements PermissionProvider {

    /**
     * Incomplete (default): Payment created but nothing confirmed as successful
     * Success: Payment successful
     * Failure: Payment failed during process
     * Declined: Payment void
     * Pending: Payment awaiting bank transfer etc
     * Process: Payment process bank transfer etc
     * Canceled: Payment canceled by member bank transfer / cheque etc
     */
    private static $db = array(
        'Status' => "Enum(array('Incomplete', 'Success', 'Failure', 'Declined', 'Canceled', 'Pending', 'Process'), 'Incomplete')",
        'Amount' => 'Currency',
        'ProcessingFee' => 'Currency',
        'Currency' => 'Varchar',
        'CurrencyRate' => 'Decimal',
        'Message' => 'Text',
        'IP' => 'Varchar',
        'ProxyIP' => 'Varchar',
        //This is used only when the payment is one of the recurring payments, when a scheduler is trying to
        //find which is the latest one for the recurring payments
        'PaymentDate' => "Date",

        //Usered for store any Exception during this payment Process.
        'ExceptionError' => 'Text'
    );

    private static $has_one = array('Receipt' => 'Receipt');

    private static $summary_fields = array(
        'PaymentMethod',
        'Created',
        'Amount',
        'ProcessingFee',
        'Currency',
        'Status',
        'Message'
    );

    private static $casting = array(
        'ConvertedAmount' => 'Currency',
        'TotalAmount' => 'Currency'
    );

    /**
     * Make payment table transactional.
     */
    private static $create_table_options = array('MySQLDatabase' => 'ENGINE=InnoDB');

    /**
     * Gets the array of default supported methods.
     */
    static function get_supported_methods() {
    	$methods = Config::inst()->get('Payment', 'supported_methods');
    	foreach($methods as $code => $active){
    		if($active && ClassInfo::exists($code)){
    			$methods[$code] = singleton($code)->i18n_singular_name();
			}
			else{
				unset($methods[$code]);
			}
    	}
        return $methods;
    }

    /**
     * Set the payment types that this site supports.
     * The classes should all be subclasses of Payment.
     *
     * @param string $method
     * @param string $title
     */
    static function add_supported_methods($method) {
        Config::inst()->update('Payment', 'supported_methods', array($method => true));
    }
    
    static function remove_supported_methods($method) {
    	Config::inst()->remove('Payment', 'supported_methods', $method);
    }

    function populateDefaults() {
        parent::populateDefaults();

        $this->Currency = SiteCurrencyConfig::current_site_currency();
        $this->CurrencyRate = 1.000000;
        $this->setClientIP();
    }
	
	public function fieldLabels($includerelations = true) {
		$labels = parent::fieldLabels($includerelations);
		
		$labels['PaymentMethod'] = _t('Payment.METHOD', 'Method');
		$labels['Created'] = _t('Payment.DATE', 'Date');
		$labels['PaymentDate'] = _t('Payment.PAID_DATE', 'Paid Date');
		$labels['Amount'] = _t('Payment.AMOUNT', 'Amount');
		$labels['ProcessingFee'] = _t('Payment.PROCESSING_FEE', 'Processing Fee');
		$labels['Currency'] = _t('Payment.CURRENCY', 'Currency');
		$labels['CurrencyRate'] = _t('Payment.CURRENCY_RATE', 'Currency Rate');
		$labels['Status'] = _t('Payment.STATUS', 'Status');
		$labels['Message'] = _t('Payment.MESSAGE', 'Message');
		$labels['ExceptionError'] = _t('Payment.EXCEPTION_ERROR', 'Exception Error');
		
		return $labels;	
	}

    /**
     * Set the IP address of the user to this payment record.
     * This isn't perfect - IP addresses can be hidden fairly easily.
     */
    function setClientIP() {
        $proxy = null;
        $ip = null;

        if(isset($_SERVER['HTTP_CLIENT_IP']))
            $ip = $_SERVER['HTTP_CLIENT_IP'];
        elseif(isset($_SERVER['REMOTE_ADDR']))
            $ip = $_SERVER['REMOTE_ADDR'];
        else
            $ip = null;

        if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            $proxy = $ip;
            $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
        }

        // Only set the IP and ProxyIP if none currently set
        if(!$this->IP)
            $this->IP = $ip;
        if(!$this->ProxyIP)
            $this->ProxyIP = $proxy;
    }

    /**
     * Returns the Payment type currently in use.
     * @return string
     */
    function PaymentMethod() {
        $method = Payment::get_supported_methods();
        if(isset($method[$this->ClassName])) {
            return $method[$this->ClassName];
        }
        return $this->class;
    }

    function getConvertedAmount() {
        return $this->TotalAmount / $this->CurrencyRate;
    }

    /**
     * Return a set of payment fields from all enabled
     * payment methods for this site, given the . {@link Payment::set_supported_methods()}
     * is used to define which methods are available.
     * @param FieldList $field
     * @param int $memberid
     * @param float $pay_amount
     * @return FieldList
     */
    static function combined_form_fields($fields, $memberid, $pay_amount) {
        $fields->push(HiddenField::create('MemberID', 'MemberID', $memberid));
        $fields->push(HiddenField::create('TotalAmount', 'TotalAmount', $pay_amount));
            
        // TODO: need change while use multiple payment
        $fields->push(HiddenField::create('Amount', 'Amount', $pay_amount));

		$support_methods = self::get_supported_methods();
        $fields->push(PaymentSetField::create('Payment', _t('Payment.PAYMENT_TYPE', 'Payment Type'), $support_methods));
        foreach($support_methods as $methodClass => $title) {
            $setFields = FieldList::create();
            if($processing_fee = Config::inst()->get($methodClass, 'processing_fee')){
				$setFields->push(HiddenField::create(sprintf('%s_ProcessingFee', $methodClass), 'ProcessingFee', $processing_fee));
                $setFields->push($showProcessingFeeField = ReadonlyField::create(sprintf('%s_ShowProcessingFee', $methodClass), _t('Payment.PROCESSING_FEE', 'Processing Fee'), DBField::create_field('Currency', $processing_fee)->Nice()));
				$showProcessingFeeField->setIncludeHiddenField(true);
            }
            $setFields->merge(singleton($methodClass)->setPaidAmount($pay_amount)->getPaymentFormFields($memberid));
            $methodFields = CompositeField::create($setFields)->addExtraClass('paymentfields')->addExtraClass('hidden')->addExtraClass($methodClass);

            $fields->push($methodFields);
        }
		
		$fields->push(SecurityPinField::create('PaymentMethodSecurityPin', _t('Payment.SECURITY_PIN', 'Security Pin')));

        return $fields;
    }

    /**
     * Return the form requirements for all the oayment methods.
     *
     * @return An array suitable for passing to RequiredFields
     */
    static function payment_requirements(&$validator, $data, $method) {
        // todo: need improve of unsupported method
        $methods = self::get_supported_methods();
        if(isset($methods[$method]) && ClassInfo::exists($method)) {
            $validator = singleton($method)->getPaymentFormRequirements($validator, $data);
        }
        else{
            $validator->validationError(
                'Payment',
                _t('Payment.INVALID_METHOD', 'Invalid Payment Method'),
                'warning'
            );
        }
    }

    function getCMSFields() {
        $fields = parent::getCMSFields();

		if($this->exists()){
			$tab = $fields->findOrMakeTab('Root.Main');
			$children = $tab->getChildren();
			foreach($children as $field){
				if($field->hasMethod('performReadonlyTransformation')){
					if($field->getName() == 'Message'){
						if(($this->Status != 'Process' && ($this->ClassName == 'BankPayment' || $this->ClassName == 'ChequePayment')) || ($this->Status != 'Pending' && $this->ClassName != 'BankPayment' && $this->ClassName != 'ChequePayment')) {
				            $fields->makeFieldReadonly('Message');
				        }
					}
					else{
						$fields->makeFieldReadonly($field->getName());
					}
				}
			}
	
	        $fields->removeByName('ReceiptID');
        }
		else {
			$fields->dataFieldByName('Status')->setSource(array('Pending' => 'Pending'));
		}

        return $fields;
    }

    function getFrontEndFields($params = null) {
        $fields = parent::getFrontendFields($params);
        $fields->removeByName('Version');
        $fields->removeByName('Status');
        $fields->removeByName('ProcessingFee');
        $fields->removeByName('Currency');
        $fields->removeByName('CurrencyRate');
        $fields->removeByName('Message');
        $fields->removeByName('Amount');
        $fields->removeByName('IP');
        $fields->removeByName('ProxyIP');
        $fields->removeByName('PaymentDate');
        $fields->removeByName('ReceiptID');
        $fields->removeByName('ExceptionError');
        return $fields;
    }

    /**
     * Return the payment form fields that should
     * be shown on the checkout order form for the
     * payment type. Example: for {@link DPSPayment},
     * this would be a set of fields to enter your
     * credit card details.
     *
     * @return FieldList
     */
    function getPaymentFormFields($memberid) {
        return $this->getFrontendFields();
    }

    /**
     * Define what fields defined in {@link Order->getPaymentFormFields()}
     * should be required.
     *
     * @see DPSPayment->getPaymentFormRequirements() for an example on how
     * this is implemented.
     *
     * @return array
     */
    function getPaymentFormRequirements($validator, $data) {
        return $validator;
    }
    
    function onBeforeWrite() {
        parent::onBeforeWrite();
        if($this->isChanged('Status') && $this->Status == 'Success' && $this->PaymentDate == '') {
            $this->PaymentDate = date('Y-m-d');
        }
        
        if($this->config()->get('processing_fee') > 0 && $this->ProcessingFee == '') {
            $this->ProcessingFee = $this->config()->get('processing_fee');
        }
    }

    function onAfterWrite() {
        parent::onAfterWrite();
        if($this->isChanged('Status') && $this->Status == 'Success') {
            $this->Receipt()->processPaymentStatus();
        }
    }
    
    function pendingPayment($data = array()) {
        user_error("Please implement pendingPayment() on $this->class", E_USER_ERROR);
    }
    
    function processPayment($data = array()) {
        user_error("Please implement processPayment() on $this->class", E_USER_ERROR);
    }
    
    function cancelPayment($data = array()) {
        user_error("Please implement cancelPayment() on $this->class", E_USER_ERROR);
    }
    
    function declinePayment($data = array()) {
        user_error("Please implement declinePayment() on $this->class", E_USER_ERROR);
    }
    
    function failurePayment($data = array()) {
        user_error("Please implement failurePayment() on $this->class", E_USER_ERROR);
    }

    function completePayment($data = array()) {
        user_error("Please implement completePayment() on $this->class", E_USER_ERROR);
    }
    
    function ProcessLink() {
        user_error("Please implement ProcessLink() on $this->class", E_USER_ERROR);
    }
    
    function RedirectLink(){
        return $this->Receipt()->TransactionType()->Link();
    }

    function handleError($e) {
        $this->ExceptionError = $e->getMessage();
        $this->write();
    }

    function getFullDetailHTML() {
        Config::inst()->update('Currency', 'currency_symbol', $this->Currency);
        return $this->renderWith($this->class);
    }

    function getAction(){
        return null;
    }
    
    function HasProcessingFee(){
        return $this->ProcessingFee > 0;
    }
    
    function getTotalAmount(){
        return $this->Amount + $this->ProcessingFee;
    }
	
	function setPaidAmount($amount){
		$this->Amount = $amount;
		return $this;
	}
	
	function canView($member = false) {
        $extended = $this->extendedCan(__FUNCTION__, $member);
        if($extended !== null) {
            return $extended;
        }
        return Permission::check('VIEW_Payment');
    }

    function canEdit($member = false) {
        return false;
    }

    function canDelete($member = false) {
        return false;
    }

    function canCreate($member = false) {
        return false;
    }
	
	function canDecline($member = false) {
        $extended = $this->extendedCan(__FUNCTION__, $member);
        if($extended !== null) {
            return $extended;
        }
		
		if($this->exists() && ($this->Status == 'Incomplete' || $this->Status == 'Pending' || $this->Status == 'Process')){
			return Permission::check('DECLINE_Payment');
		}
        return false;
    }
	
	function canApprove($member = false) {
        $extended = $this->extendedCan(__FUNCTION__, $member);
        if($extended !== null) {
            return $extended;
        }
		
		if($this->exists() && ($this->Status == 'Incomplete' || $this->Status == 'Pending' || $this->Status == 'Process')){
			return Permission::check('APPROVE_Payment');
		}
        return false;
    }

    public function providePermissions() {
        return array(
            'VIEW_Payment' => array(
                'name' => _t('Payment.PERMISSION_VIEW', 'Allow view access right'),
                'category' => _t('Payment.PERMISSIONS_CATEGORY', 'Payment Method')
            ),
            'DECLINE_Payment' => array(
                'name' => _t('Payment.PERMISSION_DECLINE', 'Allow decline access right'),
                'category' => _t('Payment.PERMISSIONS_CATEGORY', 'Payment Method')
            ),
            'APPROVE_Payment' => array(
                'name' => _t('Payment.PERMISSION_APPROVE', 'Allow approve access right'),
                'category' => _t('Payment.PERMISSIONS_CATEGORY', 'Payment Method')
            )
        );
    }
}

class Payment_Validator extends RequiredFields {
    function php($data) {
        $valid = parent::php($data);
        Payment::payment_requirements($this, $data, $data['Payment']);
        $this->extend('updatePaymentValidator', $this, $data, $this->form);
        return $this->getErrors();
    }

}
?>